'use strict';

var canvas = document.querySelector('canvas');
var gl = canvas.getContext("webgl");
var mrt = gl.getExtension('WEBGL_draw_buffers');    // mrt = Multi Render Target
var texf = gl.getExtension('OES_texture_float');
var pixel_size = [0, 0];
console.log(gl.getSupportedExtensions());

/*
var fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
gl.framebufferTexture2D(gl.FRAMEBUFFER, mrt.COLOR_ATTACHMENT0_WEBGL, gl.TEXTURE_2D, tx[0], 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, mrt.COLOR_ATTACHMENT1_WEBGL, gl.TEXTURE_2D, tx[1], 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, mrt.COLOR_ATTACHMENT2_WEBGL, gl.TEXTURE_2D, tx[2], 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, mrt.COLOR_ATTACHMENT3_WEBGL, gl.TEXTURE_2D, tx[3], 0);

mrt.drawBuffersWEBGL([
    mrt.COLOR_ATTACHMENT0_WEBGL, // gl_FragData[0]
    mrt.COLOR_ATTACHMENT1_WEBGL, // gl_FragData[1]
    mrt.COLOR_ATTACHMENT2_WEBGL, // gl_FragData[2]
    mrt.COLOR_ATTACHMENT3_WEBGL, // gl_FragData[3]
]);

// in shader:
#extension GL_EXT_draw_buffers : require
*/

function resize_canvas()
{
    var scale_ratio = window.devicePixelRatio || 1;
    canvas.width  = 16*50;
    canvas.height =  9*50;
    canvas.style.width  = parseInt(canvas.width  / scale_ratio) + "px";
    canvas.style.height = parseInt(canvas.height / scale_ratio) + "px";
    gl.viewport(0, 0, canvas.width, canvas.height);
    pixel_size = [1 / canvas.width, 1 / canvas.height];
    console.log(canvas);
}
resize_canvas();
//window.addEventListener("resize", resize_canvas);

load_files([
    'blur-shaders/draw.vert',
    'blur-shaders/draw.frag',
    'blur-shaders/blur.vert',
    'blur-shaders/blur.frag',
    'blur-shaders/sblur.vert',
    'blur-shaders/sblur.frag',
])
    .then(prepare_render)
    .catch(alert);

function prepare_render(files)
{
    console.log(files);

    var programs = {};
    var unis = {};
    var attrs = {};
    var setups = {};

    function load_program(name, setup)
    {
        programs[name] = build_shader_program([
            ['vertex'  , files['blur-shaders/' + name + '.vert']],
            ['fragment', files['blur-shaders/' + name + '.frag']],
        ]);
        unis[name] = uniforms_of(programs[name]);
        attrs[name] = attributes_of(programs[name]);
        setups[name] = setup;
    }

    function use_program(name)
    {
        gl.useProgram(programs[name]);

        var setup_attrs = setups[name].attrs;
        for (var attr_name in setup_attrs) {
            gl.enableVertexAttribArray(attrs[name](attr_name));
            gl.vertexAttribPointer(attrs[name](attr_name), ...setup_attrs[attr_name]);
        }
    }

    var buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        // fullscreen triangle (2f):
        -1, -1,
         3, -1,
        -1,  3,
        // scene data: coords (2f), uvs(2f)
        -0.5, -0.5,    0, 0,
        -0.5,  0.5,    0, 1,
         0.5,  0.0,    1, 0.5,
    ]), gl.STATIC_DRAW)

    load_program('blur', {
        attrs: {
            coord: [2, gl.FLOAT, false, 2*4, 0],
        },
    });

    load_program('sblur', {
        attrs: {
            coord: [2, gl.FLOAT, false, 2*4, 0],
        },
    });

    load_program('draw', {
        attrs: {
            coord: [2, gl.FLOAT, false, 4*4, 3*2*4 + 0  ],
            uv   : [2, gl.FLOAT, false, 4*4, 3*2*4 + 2*4],
        },
    });

    var scene_tex = gl.createTexture();
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, scene_tex);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);     // `
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);     // `
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);   //  > required for non NPOT texture
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);   // /
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, canvas.width, canvas.height, 0, gl.RGBA, gl.FLOAT, null);

    var scene_fb = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, scene_fb);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, scene_tex, 0);

    var blur_pass_tex = gl.createTexture();
    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, blur_pass_tex);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);     // `
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);     // `
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);   //  > required for non NPOT texture
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);   // /
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, canvas.width, canvas.height, 0, gl.RGBA, gl.FLOAT, null);

    var blur_pass_fb = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, blur_pass_fb);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, blur_pass_tex, 0);

    function render(time)
    {
        use_program('draw');
        gl.bindFramebuffer(gl.FRAMEBUFFER, scene_fb);
        gl.clearColor(0, 0, 0, 0);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.TRIANGLES, 0, 3);

        // use separable filter
        if (control_separable.checked) {
            use_program('sblur');
            gl.uniform1f(unis.sblur("time"), (time / 10) % 10);
            gl.uniform1i(unis.sblur("algorithm"), parseInt(control_algorithm.value));
            gl.uniform1i(unis.sblur("num_samples"), parseInt(control_samples.value));
            gl.uniform1f(unis.sblur("radius"), parseFloat(control_radius.value));

            gl.bindFramebuffer(gl.FRAMEBUFFER, blur_pass_fb);
            gl.clearColor(1, 0, 0, 1);
            gl.clear(gl.COLOR_BUFFER_BIT);
            gl.uniform1i(unis.sblur("tex"), 0);
            gl.uniform2f(unis.sblur("pixel_size"), pixel_size[0], 0);
            gl.drawArrays(gl.TRIANGLES, 0, 3);

            gl.bindFramebuffer(gl.FRAMEBUFFER, null);
            gl.clearColor(0, 1, 0, 1);
            gl.clear(gl.COLOR_BUFFER_BIT);
            gl.uniform1i(unis.sblur("tex"), 1);
            gl.uniform2f(unis.sblur("pixel_size"), 0, pixel_size[1]);
            gl.drawArrays(gl.TRIANGLES, 0, 3);
        }

        // use 2d filter
        else {
            gl.bindFramebuffer(gl.FRAMEBUFFER, null);
            gl.clearColor(1, 1, 0, 1);
            gl.clear(gl.COLOR_BUFFER_BIT);
            use_program('blur');
            gl.uniform1i(unis.blur("tex"), 0);
            gl.uniform1f(unis.blur("time"), (time / 10) % 10);
            gl.uniform2fv(unis.blur("pixel_size"), pixel_size);
            gl.uniform1i(unis.blur("algorithm"), parseInt(control_algorithm.value));
            gl.uniform1i(unis.blur("num_samples"), parseInt(control_samples.value));
            gl.uniform1f(unis.blur("radius"), parseFloat(control_radius.value));
            gl.drawArrays(gl.TRIANGLES, 0, 3);
        }
    }
    //render(0);

    var frozen_time = null;
    ((render, t) => render(render, t))((self, t) => {
        render(frozen_time == null ? t / 1000 : frozen_time);
        if (frozen_time == null && !control_time.checked) frozen_time = t / 1000;
        if (frozen_time != null &&  control_time.checked) frozen_time = null;
        requestAnimationFrame(t => self(self, t));
    })
}

// ShaderType => 'fragment' | 'vertex'
// :: ShaderType -> glEnum
function shader_type_to_gl(type)
{
    if (typeof(type) !== 'string')
        throw "shader type must be a string";

    switch (type) {
        case 'vertex'  : return gl.VERTEX_SHADER;
        case 'fragment': return gl.FRAGMENT_SHADER;
        default: throw "invalid shader type: " + type;
    }
}

// :: [ [ ShaderType, String ], ... ] -> ShaderProgram
function build_shader_program(shaders)
{
    var compiled_shaders = shaders.map(shader => {
        var type   = shader[0];
        var source = shader[1];

        var s = gl.createShader(shader_type_to_gl(type));
        gl.shaderSource(s, source);
        gl.compileShader(s);

        if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
            console.error(shaders, shader);
            throw gl.getShaderInfoLog(s);
        }

        return s;
    });

    var program = gl.createProgram();
    compiled_shaders.forEach(shader => gl.attachShader(program, shader));
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        console.error(shaders);
        throw gl.getProgramInfoLog(program);
    }

    return program;
}

function attributes_of(program)
{
    var cache = {}
    return name => {
        if (cache[name] == null)
            cache[name] = gl.getAttribLocation(program, name);
        if (cache[name] == null) {
            console.error(name, cache[name], program);
            throw "no such attribute: " + name;
        }
        return cache[name];
    };
}

function uniforms_of(program)
{
    var cache = {}
    return name => {
        if (cache[name] == null)
            cache[name] = gl.getUniformLocation(program, name);
        if (cache[name] == null || cache[name] === -1) {
            console.error(name, cache[name], program);
            throw "no such uniform: " + name;
        }
        return cache[name];
    };
}


function load_files(files)
{
    return new Promise((ok, fail) => {
        var contents = {}
        var num_files = files.length;
        var num_loaded_files = 0;

        var loaded = filename => content => {
            contents[filename] = content
            if (++num_loaded_files === num_files)
                ok(contents);
        };
        var error = filename => reason => {
            fail("load " + filename + " failed: " + reason)
        };

        files.forEach(
            filename => ajax_get(filename)
                .then(loaded(filename))
                .catch(error(filename))
        );
    });
}

function ajax_get(url)
{
    return new Promise((ok, fail) => {
        var req = new XMLHttpRequest();
        req.onload = () => req.status >= 200 && req.status < 300 ? ok(req.response) : fail(req.statusText);
        req.onerror = () => fail(req.statusText);
        req.onabort = () => fail(req.statusText);

        req.responseType = 'text';
        req.open('GET', url);
        req.send();
    });
}

